package fabricsdk_go

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"gitee.com/wayjin/fabricsdk-go/cryptoutil"
	"github.com/hyperledger/fabric/core/chaincode"
	"github.com/hyperledger/fabric/core/chaincode/platforms"
	"github.com/hyperledger/fabric/core/chaincode/platforms/car"
	"github.com/hyperledger/fabric/core/chaincode/platforms/golang"
	"github.com/hyperledger/fabric/core/chaincode/platforms/java"
	"github.com/hyperledger/fabric/core/chaincode/platforms/node"
	"github.com/hyperledger/fabric/core/chaincode/shim"
	"github.com/hyperledger/fabric/core/container"
	"github.com/hyperledger/fabric/peer/common/api"
	pcommon "github.com/hyperledger/fabric/protos/common"
	"github.com/hyperledger/fabric/protos/peer"
	putils "github.com/hyperledger/fabric/protos/utils"
	"github.com/pkg/errors"
	"strings"
	"time"
)

type ChaincodeSpec struct {
	Name       string
	Version    string
	Lang       string
	Args       []byte
	Path       string
	Transient  string
	Collection []byte
	PolicyMarshaled []byte
}

type ChaincodeInvokeResponse struct {
	TxID  string
	Status int
	Payload []byte
	Message []byte
}


type chaincodeFactory struct {
	endorserClients []peer.EndorserClient
	deliverClients  []api.PeerDeliverClient
	broadcast       BroadcastClient
	peerAddresses   []string
	tlsCert         tls.Certificate
	ordererClient   *ordererClient
}

func invokeChaincode(spec ChaincodeSpec, cf *chaincodeFactory, signer cryptoutil.Signer, channelID string, waitForEventTimeout time.Duration) error {
	invokeReq := chaincodeRequest{
		spec:   spec,
		invoke: true,
		txID:   "",
	}

	if waitForEventTimeout == 0 {
		invokeReq.waitForEvent = false
	} else {
		invokeReq.waitForEvent = true
		invokeReq.waitForEventTimeout = waitForEventTimeout
	}

	proposalResp, err := invokeAndQueryChaincode(signer, invokeReq, cf, channelID)
	if err != nil {
		return errors.Errorf("%s - proposal response: %v", err, proposalResp)
	}

	pRespPayload, err := putils.GetProposalResponsePayload(proposalResp.Payload)
	if err != nil {
		return errors.WithMessage(err, "error while unmarshaling proposal response payload")
	}

	ca, err := putils.GetChaincodeAction(pRespPayload.Extension)
	if err != nil {
		return errors.WithMessage(err, "error while unmarshaling chaincode action")
	}

	if proposalResp.Endorsement == nil {
		return errors.Errorf("endorsement failure during invoke. response: %v", proposalResp.Response)
	}

	//fmt.Println(ca.Response)
	if ca.Response.Status >= shim.ERRORTHRESHOLD {
		fmt.Println("invalid status")
		return errors.Errorf("invoke response with invalid status: %v", ca.Response.Status)
	}
	//fmt.Printf("%x\n", pRespPayload.ProposalHash)
	//fmt.Println("result:", string(ca.Results))
	//fmt.Println("chaincodeId:", ca.ChaincodeId)

	return nil
}

// 查询链码
func queryChaincode(spec ChaincodeSpec, cf *chaincodeFactory, signer cryptoutil.Signer, channelID string, queryHex bool) error {
	queryReq := chaincodeRequest{
		spec: spec,
		invoke: false,
		txID: "",
	}

	proposalResp, err := invokeAndQueryChaincode(signer, queryReq, cf, channelID)
	if err != nil {
		return fmt.Errorf("query chaincode fail: %v", err)
	}

	if proposalResp == nil {
		return errors.New("error during query: received nil proposal response")
	}
	if proposalResp.Endorsement == nil {
		return errors.Errorf("endorsement failure during query. response: %v", proposalResp.Response)
	}
	if queryHex {
		fmt.Printf("%x\n", proposalResp.Response.Payload)
		return nil
	}
	fmt.Println(string(proposalResp.Response.Payload))
	return nil
}

type chaincodeRequest struct {
	spec                ChaincodeSpec
	txID                string
	invoke              bool
	waitForEvent        bool
	waitForEventTimeout time.Duration
	queryHex  bool
}

var platformRegistry = platforms.NewRegistry(
	&golang.Platform{},
	&car.Platform{},
	&java.Platform{},
	&node.Platform{},
)

func (cq chaincodeRequest) invocation() (*peer.ChaincodeInvocationSpec, error) {
	spec := &peer.ChaincodeSpec{}
	// Build the spec
	input := &peer.ChaincodeInput{}
	err := json.Unmarshal(cq.spec.Args, &input)
	if err != nil {
		return nil, errors.Wrap(err, "chaincode argument error")
	}
	chaincodeLang := strings.ToUpper(cq.spec.Lang)
	spec = &peer.ChaincodeSpec{
		Type:        peer.ChaincodeSpec_Type(peer.ChaincodeSpec_Type_value[chaincodeLang]),
		ChaincodeId: &peer.ChaincodeID{Path: cq.spec.Path, Name: cq.spec.Name, Version: cq.spec.Version},
		Input:       input,
	}
	return &peer.ChaincodeInvocationSpec{ChaincodeSpec: spec}, nil
}

func (cqr chaincodeRequest) getChaincodeSpec() (*peer.ChaincodeSpec, error) {
	spec := &peer.ChaincodeSpec{}
	// Build the spec
	input := &peer.ChaincodeInput{}
	err := json.Unmarshal(cqr.spec.Args, &input)
	if err != nil {
		return nil, errors.Wrap(err, "chaincode argument error")
	}
	chaincodeLang := strings.ToUpper(cqr.spec.Lang)
	spec = &peer.ChaincodeSpec{
		Type:        peer.ChaincodeSpec_Type(peer.ChaincodeSpec_Type_value[chaincodeLang]),
		ChaincodeId: &peer.ChaincodeID{Path: cqr.spec.Path, Name: cqr.spec.Name, Version: cqr.spec.Version},
		Input:       input,
	}
	return spec, nil
}

func (cqr chaincodeRequest) getChaicodeDeploySpec(crtPkg bool) (*peer.ChaincodeDeploymentSpec, error) {
	spec, err := cqr.getChaincodeSpec()
	if err != nil {
		return nil, err
	}

	var codePackageBytes []byte
	if chaincode.IsDevMode() == false && crtPkg {
		if spec == nil {
			return nil, errors.New("expected chaincode specification, nil received")
		}

		if err = platformRegistry.ValidateSpec(spec.CCType(), spec.Path()); err != nil {
			return nil, err
		}

		codePackageBytes, err = container.GetChaincodePackageBytes(platformRegistry, spec)
		if err != nil {
			err = errors.WithMessage(err, "error getting chaincode package bytes")
			return nil, err
		}
		if err != nil {
			err = errors.WithMessage(err, "error getting chaincode package bytes")
			return nil, err
		}
	}
	chaincodeDeploymentSpec := &peer.ChaincodeDeploymentSpec{ChaincodeSpec: spec, CodePackage: codePackageBytes}
	return chaincodeDeploymentSpec, nil
}

func invokeAndQueryChaincode(signer cryptoutil.Signer, req chaincodeRequest, cf *chaincodeFactory, channelID string) (*peer.ProposalResponse, error) {
	invocation, err := req.invocation()
	if err != nil {
		return nil, err
	}
	// extract the transient field if it exists
	var tMap map[string][]byte
	if req.spec.Transient != "" {
		if err := json.Unmarshal([]byte(req.spec.Transient), &tMap); err != nil {
			return nil, errors.Wrap(err, "error parsing transient string")
		}
	}
	creator, err := signer.Serialize()
	if err != nil {
		return nil, err
	}
	funcName := "invoke"
	if !req.invoke {
		funcName = "query"
	}

	prop, txid, err := putils.CreateChaincodeProposalWithTxIDAndTransient(pcommon.HeaderType_ENDORSER_TRANSACTION, channelID, invocation, creator, req.txID, tMap)
	if err != nil {
		return nil, errors.WithMessage(err, fmt.Sprintf("error creating proposal for %s", funcName))
	}
	signedProp, err := cryptoutil.GetSignedProposal(prop, signer)
	if err != nil {
		return nil, errors.WithMessage(err, fmt.Sprintf("error creating signed proposal for %s", funcName))
	}

	var responses []*peer.ProposalResponse
	for _, endorser := range cf.endorserClients {
		proposalResp, err := endorser.ProcessProposal(context.Background(), signedProp)
		if err != nil {
			return nil, errors.WithMessage(err, fmt.Sprintf("error endorsing %s", funcName))
		}
		responses = append(responses, proposalResp)
	}

	if len(responses) == 0 {
		// this should only happen if some new code has introduced a bug
		return nil, errors.New("no proposal responses received - this might indicate a bug")
	}

	proposalResp := responses[0]
	if req.invoke {
		if proposalResp != nil {
			if proposalResp.Response.Status >= shim.ERRORTHRESHOLD {
				return proposalResp, nil
			}

			env, err := createSignedTx(prop, signer, responses...)
			if err != nil {
				return proposalResp, errors.WithMessage(err, "could not assemble transaction")
			}

			var dg *deliverGroup
			var ctx context.Context
			if req.waitForEvent {
				var cancelFunc context.CancelFunc
				ctx, cancelFunc = context.WithTimeout(context.Background(), req.waitForEventTimeout)
				defer cancelFunc()

				dg = newDeliverGroup(cf.deliverClients, cf.peerAddresses, cf.tlsCert, signer, channelID, txid)
				// connect to deliver service on all peers
				err := dg.Connect(ctx)
				if err != nil {
					return nil, err
				}
			}

			// 这里的问题
			broadcast, err := cf.ordererClient.GetBroadcastClient()
			if err != nil {
				return nil, errors.WithMessage(err, "get broadcast fail")
			}
			defer broadcast.Close()

			if err = broadcast.Send(env); err != nil {
				return proposalResp, errors.WithMessage(err, fmt.Sprintf("error sending transaction for %s", funcName))
			}
			if dg != nil && ctx != nil {
				// wait for event that contains the txid from all peers
				err = dg.Wait(ctx)
				if err != nil {
					return nil, err
				}
			}
		}

		return proposalResp, nil
	}

	return proposalResp, nil
}

// 列出已安装或实例化的链码
func listChaincode(signer cryptoutil.Signer) {
	// TODO
}

// 安装链码
func installChaincode(signer cryptoutil.Signer, req chaincodeRequest) error {
	// TODO
	return nil
}

// 实例化链码
func instantiateChaincode(signer cryptoutil.Signer, req chaincodeRequest, cf *chaincodeFactory, channelID string) error {
	spec, err := req.getChaicodeDeploySpec(false)
	if err != nil {
		return err
	}
	creator, err := signer.Serialize()
	if err != nil {
		return fmt.Errorf("error serializing identity for %s: %s", "", err)
	}

	prop, _, err := putils.CreateDeployProposalFromCDS(channelID, spec, creator, req.spec.PolicyMarshaled, []byte("escc"), []byte("vscc"), req.spec.PolicyMarshaled)
	if err != nil {
		return fmt.Errorf("error creating proposal  %s: %s", "instantiate", err)
	}

	signedProp, err := cryptoutil.GetSignedProposal(prop, signer)
	if err != nil {
		return fmt.Errorf("error creating signed proposal  %s: %s", "instantiate", err)
	}

	endorser := cf.endorserClients[0]
	resp, err := endorser.ProcessProposal(context.Background(), signedProp)
	if err != nil {
		return err
	}

	if resp != nil {
		env, err := createSignedTx(prop, signer, resp)
		if err != nil {
			return fmt.Errorf("could not assemble transaction, err %s", err)
		}

		broadcast, err := cf.ordererClient.GetBroadcastClient()
		if err != nil {
			return err
		}
		err = broadcast.Send(env)
		if err != nil {
			return err
		}
		return nil
	}
	return nil
}